package provisioning;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.math.stat.descriptive.rank.Percentile;

import commons.cloud.MachineType;
import commons.cloud.Provider;
import commons.cloud.Request;
import commons.config.Configuration;
import commons.io.Checkpointer;
import commons.io.GEISTWorkloadParser;
import commons.io.WorkloadParser;
import commons.sim.provisioningheuristics.MachineStatistics;
import commons.sim.util.SaaSAppProperties;

import edu.uah.math.distributions.BinomialDistribution;


class FictiousMachine {
	
	int currentNumberOfRequests;
	double meanDemandTime;

	public FictiousMachine(){
		this.currentNumberOfRequests = 0;
		this.meanDemandTime = 0;
	}
	
}

/**
 * This class represents the DPS business logic modified from original RANJAN. Here some statistics of current
 * available machines (i.e, utilisation) is used to verify if new machines need to be added to 
 * an application tier, or if some machines can be removed from any application tier. After the number of servers needed
 * is calculated, the DPS verifies if any powerful reserved machine is available to be added and, if not, accelerator nodes
 * are purchased from the cloud provider.
 * 
 * @author David Candeia Medeiros Maia - davidcmm@lsd.ufcg.edu.br
 *
 */
public class OptimalProvisioningSystemForHeterogeneousMachines extends DynamicProvisioningSystem {

	private static final int QUANTUM_SIZE_IN_MILLIS = 100;

	protected MachineType[] acceleratorTypes = {MachineType.M1_SMALL};
	
	private int oneHourTick;
	private long currentTick;
	private Request[] leftOver;
	private WorkloadParser[] parsers;
	private double[] currentRequestsCounter;
	private double[] nextRequestsCounter;
	private double[] instancesCounter;
	private long SLA;
	private double OPTIMAL_UTILIZATION = 0.6;//Resource target utilization
	
	private long totalMeanToProcess;
	private int numberOfRequests;
	public LinkedList<Integer> machinesPerHour;
	
	//Debug
	private FileWriter writer; 
	
	/**
	 * Default constructor.
	 */
	public OptimalProvisioningSystemForHeterogeneousMachines() {
		super();
		
		String[] workloadFiles = Configuration.getInstance().getWorkloads();
		this.oneHourTick = 1000 * 60 * 60;
		this.currentTick = Checkpointer.loadSimulationInfo().getCurrentDayInMillis() + oneHourTick;
		this.leftOver = new Request[workloadFiles.length];

		this.SLA = Configuration.getInstance().getLong(SaaSAppProperties.APPLICATION_SLA_MAX_RESPONSE_TIME);
		this.nextRequestsCounter = new double[36000];
		int numberOfIntervals = (int) Math.ceil(oneHourTick / this.SLA);
		this.instancesCounter = new double[numberOfIntervals];
		
		this.machinesPerHour =  new LinkedList<Integer>();
		
		try {
			this.writer = new FileWriter(new File("machineConsumption.dat"), true);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public boolean isOptimal() {
		return true;
	}
	
	/**
	 * Reading future workload and requesting machines!
	 */
	@Override
	public void sendStatistics(long now, MachineStatistics statistics, int tier) {
		
		if(parsers == null){
			String[] workloadFiles = Configuration.getInstance().getWorkloads();
			this.parsers = new WorkloadParser[workloadFiles.length];
			for(int i = 0; i < workloadFiles.length; i++){
				this.parsers[i] = new GEISTWorkloadParser(workloadFiles[i]);
			}
		}
		
		long nextDeadline = now + this.SLA;
		int instancesCounterIndex = 0;
		while(nextDeadline < currentTick){

			List<Request> currentRequests = new ArrayList<Request>();
			
			for (int i = 0; i < leftOver.length; i++){//Gathering requests that were left previously
				Request left = leftOver[i];
				if(left != null){
					if(left.getArrivalTimeInMillis() < nextDeadline){
						currentRequests.add(left);
						leftOver[i] = null;
					}
				}
			}
			
			for (int i = 0; i < parsers.length; i++) {//Reading new requests
				if(leftOver[i] == null){
					WorkloadParser<Request> parser = parsers[i];
					while(parser.hasNext()){
						Request next = parser.next();
						if(next.getArrivalTimeInMillis() < nextDeadline){
							currentRequests.add(next);
						}else{
							leftOver[i] = next;
							break;
						}
					}
				}
			}
			
			//Allocating requests to machines
			List<FictiousMachine> machines = new ArrayList<FictiousMachine>();
			machines.add(new FictiousMachine());
			
			int nextToUse = 0;
			for(Request request : currentRequests){
				boolean allocated = false;
				
				nextToUse = (nextToUse) % machines.size();
				FictiousMachine machine = machines.get(nextToUse++);
				
				double newMeanDemand = (machine.currentNumberOfRequests * machine.meanDemandTime + request.getMaximumToProcess()) / (1 + machine.currentNumberOfRequests);
				double estimatedFinishingTime = newMeanDemand * (1+machine.currentNumberOfRequests);
				if(estimatedFinishingTime <= this.OPTIMAL_UTILIZATION * this.SLA){
					machine.currentNumberOfRequests++;
					machine.meanDemandTime = newMeanDemand;
					allocated = true;
				}
				
				if(!allocated){//Create a new machine
					FictiousMachine newMachine = new FictiousMachine();
					newMachine.currentNumberOfRequests++;
					newMachine.meanDemandTime = request.getMaximumToProcess();
//					machines.add(0, newMachine);
					machines.add(newMachine);
//					nextToUse++;
				}
			}
			
			//Updating next deadline
			nextDeadline += this.SLA;
			
			//Updating instances counter
			this.instancesCounter[instancesCounterIndex] = machines.size();
			instancesCounterIndex++;
		}
		
		this.currentTick += oneHourTick;
		
		//Calculating number of machines considering statistics of current infrastructure
		evaluateNumberOfServersToAdd(tier, statistics, now);
	}
	
	/**
	 * Calculates the number of servers to be added or removed at next execution interval
	 * @param tier
	 * @param totalNumberOfServers Current number of machines
	 * @param now
	 */
	private void evaluateNumberOfServersToAdd(int tier, MachineStatistics statistics, long now) {
		
		Percentile percentile = new Percentile(Configuration.getInstance().getOptimalDPSPercentile());
		double maximumDemand = Math.ceil(percentile.evaluate(instancesCounter));
		
//		List b = Arrays.asList(ArrayUtils.toObject(instancesCounter));

		//FIXME: A Xlarge machine is not only 4 times greater than a small (considering cores), but it is 8 times greater (cores + ec2 units)
		//Computing the total amount of available cores
		long currentNumberOfServers = 0;
		Map<MachineType, Integer> currentMachinesPerType = statistics.getCurrentMachinesPerType();
		for(MachineType type : currentMachinesPerType.keySet()){
			currentNumberOfServers += type.getTotalCapacity() * (currentMachinesPerType.get(type)); 
		}
		
		int serversNeeded = (int)Math.ceil(maximumDemand);
		this.machinesPerHour.add(serversNeeded);
		
		long numberOfServersToAdd = serversNeeded - currentNumberOfServers;
		
		System.err.print(now + "\tSummary\t"+serversNeeded+"\t"+numberOfServersToAdd+"\t"+currentNumberOfServers+"\t");
		
		if(numberOfServersToAdd > 0){//Adding servers
//			System.err.print("Purch:\t"+serversNeeded+"\t"+numberOfServersToAdd+"\t"+currentNumberOfServers+"\t");
			System.err.print("Purch\t");
			contractMachines(tier, numberOfServersToAdd);
		}else if(numberOfServersToAdd < 0){//Removing servers
//			System.err.print("Rel:\t"+serversNeeded+"\t"+numberOfServersToAdd+"\t"+currentNumberOfServers+"\t");
			System.err.print("Releas\t");
			//Removing servers and accounting the amount of cores removed
			long numberOfServersToRemove = -numberOfServersToAdd;
			while(numberOfServersToRemove > 0){
				int coresRemoved = configurable.removeServer(tier, false);
				if(coresRemoved > 0){
					numberOfServersToRemove -= coresRemoved;
					System.err.print(""+coresRemoved+"\t");
				}else{
					break;
				}
			}
		}
		
		System.err.println("");

		//Debug resource consumption information
		if(Configuration.getInstance().isDebugMode()){
			for (Provider provider : providers) {
//				Map<MachineType, Integer> reservedResources = provider.getAmountOfReservedResources();
//				Map<MachineType, Integer> onDemandResources = provider.getAmountOfOnDemandResources();
//	
//				System.err.println("Provider:" + provider.getName()+";\tDPS:\t"+statistics.getCurrentMachinesPerType().toString());
//				System.err.println("Provider:" + provider.getName()+";Reserved:"+reservedResources+";OnDemand:"+onDemandResources);
			}
		}
	}
	
	/**
	 * Buying machines at IaaS providers and adding them to IT infrastructure
	 * @param tier
	 * @param numberOfServersToAdd
	 */
	private void contractMachines(int tier, long numberOfServersToAdd) {
		int serversAdded = 0;
		
		List<MachineType> typeList = Arrays.asList(MachineType.values());
		Collections.reverse(typeList);
		
		//Sorting providers according to their reservation limits: considering Amazon EC2 the order is Light Market, Medium market and Heavy Market
		Provider [] sortedProviders = providers;
		Arrays.sort(sortedProviders);
		
		System.err.print("RES:\t");
		
		for(MachineType machineType: typeList){//TODO test which order is the best
			for(int i = sortedProviders.length-1; i >= 0; i--){//Starting with heavy providers!
				Provider provider = sortedProviders[i];
				while(provider.canBuyMachine(true, machineType) && 
						serversAdded + machineType.getTotalCapacity() <= numberOfServersToAdd){
					configurable.addServer(tier, provider.buyMachine(true, machineType), true);
					serversAdded += machineType.getTotalCapacity();
					
					System.err.print(machineType.getTotalCapacity()+"\t");
				}
				if(serversAdded == numberOfServersToAdd){
					break;
				}
			}
			
			//DEBUG
//			System.err.print("RES:\t"+machineType.toString()+"\t"+debugAmount+"\t");
			
			if(serversAdded == numberOfServersToAdd){
				break;
			}
		}
		
		//If servers are still needed ...
		if(serversAdded < numberOfServersToAdd){
			
			//Applying on-demand market risk ...
			numberOfServersToAdd = (numberOfServersToAdd - serversAdded);
			serversAdded = 0;

			numberOfServersToAdd = calculateNumberOfServersFromOnDemand(numberOfServersToAdd);
			
			System.err.print("ON:\t");
			
			for(MachineType machineType : this.acceleratorTypes){
				for (Provider provider : sortedProviders) {
					while(provider.canBuyMachine(false, machineType) && 
							serversAdded + machineType.getTotalCapacity() <= numberOfServersToAdd){
						configurable.addServer(tier, provider.buyMachine(false, machineType), true);
						serversAdded += machineType.getTotalCapacity();
						
						System.err.print(machineType.getTotalCapacity()+"\t");
					}
					if(serversAdded == numberOfServersToAdd){
						break;
					}
				}
				
				//DEBUG
//				System.err.print("ON:\t"+machineType.ordinal()+"\t"+serversAdded+"\n");
				
				if(serversAdded == numberOfServersToAdd){
					break;
				}
			}
		}
	}
	
	/**
	 * This method calculates the number of resources that are successfully acquired from the IaaS on-demand market
	 * considering that there is a DoS risk
	 * @param numberOfServersToAdd The amount of resources required by the Optimal DPS system
	 * @return The number of resources that were successfully acquired from the IaaS on-demand market
	 */
	private long calculateNumberOfServersFromOnDemand(long numberOfServersToAdd) {
		int trials = (int) numberOfServersToAdd;
		double p = 1.0 - Configuration.getInstance().getIaaSOnDemandRisk();
		BinomialDistribution dist = new BinomialDistribution(trials, p);
		
		long result = (long)dist.simulate();
		
		if(Configuration.getInstance().isDebugMode()){
//			System.err.println(Configuration.getInstance().getIaaSOnDemandRisk()+"\t"+result);
		}
		
		return result;
	}

	@Override
	public void requestQueued(long timeMilliSeconds, Request request, int tier) {
		reportLostRequest(request);
	}
}
